
import * as d3 from "d3";
// import {isEqual} from "../lib/underscore.js";
import {isEqual} from "underscore";
import {ø} from "../util/objects.js";

/**
 * A Model is a map that emits events when its entries change. The set of allowable keys is fixed at creation time
 * using the specified initial state.
 *
 * const model = createModel({a: 1, b: 2});
 * model.on("change",   (delta, old, meta) => console.log(`change ${delta}, ${old}, ${meta}`));
 * model.on("change:b", (delta, old, meta) => console.log(`change:b ${delta}, ${old}, ${meta}`));
 *
 * model.get("a");             // 1
 * model.set("a", 2);          // "change {a: 2}, {a: 1}, undefined"
 * model.save({a: 3}, "foo");  // "change {a: 3}, {a: 2}, foo"
 * model.save({a: 4, b: 0});   // "change:b 0, 2, undefined" and "change {a: 4, b: 0}, {a: 3, b: 2}, undefined"
 *
 * Attribute changes are detected using underscore's isEqual deep comparison.
 *
 * UNDONE: Given all the constraints, probably best to build own implementation of dispatcher.
 *
 * @param {Object} initialState the fixed set of keys and their initial values.
 * @returns {Model}
 */
export function createModel(initialState) {

    const keys = Object.keys(initialState);
    const dispatch = d3.dispatch.apply(undefined, ["change"].concat(keys.map(key => `change:${key}`)));
    const attributes = ø(initialState);
    
    // const store = useStore();

    function interpret(typenames) {
        return typenames.trim().split(/^|\s+/);
    }

    return new class Model {

        /**
         * Registers a callback for the specified typenames. Basically the same as D3 dispatch.on().
         *
         * There are two event patterns:
         *
         *     "change"        fired when any attribute changes
         *                     callback ƒ(delta, old, meta)
         *                         delta: map of changed attributes
         *                         old:   map of original attributes
         *                         meta:  the (optional) object specified on the call to save()
         *
         *     "change:<key>"  fired when attribute for <key> changes
         *                     callback ƒ(delta, old, meta)
         *                         delta: the new attribute value
         *                         old:   the old attribute value
         *                         meta:  the (optional) object specified on the call to save()
         *
         * model.on("change",     ƒ);  // adds handler for "change" event
         * model.on("change",     ƒ);  // replaces previous handler for "change" event
         * model.on("change.foo", ƒ);  // adds handler for "change" event with name "foo"
         * model.on("change.bar", ƒ);  // adds handler for "change" event with name "bar"
         *
         * model.on("change:a change:b", ƒ);  // adds handler for both "change:a" and "change:b"
         *
         * model.on("change");        // returns handler for "change" event
         * model.on("change.foo");    // returns handler for "change" event for name "foo"
         * model.on("change", null);  // removes the handler for "change" event
         * model.on(".foo", null);    // removes all handlers for name "foo"
         *
         * @param {string} typenames
         * @param {Function} [callback] ƒ(delta, old, meta)
         * @returns {Model|Function} this model instance if a callback specified, otherwise the callback
         *          registered for the specified typename.
         */
        on(typenames, callback) {
            const tn = interpret(typenames).join(" ");
            // console.log("on",typenames)
            return arguments.length < 2 ? dispatch.on(tn) : (dispatch.on(tn, callback), this);
        }

        /**
         * @param {string} key
         * @returns {*}
         */
        get(key) {
            return attributes[key];
        }

        /** @returns {Object} a shallow copy of this model's attributes */
        getAll() {
            return ø(attributes);
        }

        /**
         * Examples:
         *     model.save({a: 1});                      // raises "change" and "change:a" events
         *     model.save({a: 1, b: 2}, {foo: "bar"});  // raises "change", "change:a", and "change:b" events
         *
         * @param {Object} changes the entries to change on this model. Events are raised if changes from this model's
         *                 current state are found using underscore's isEqual deep comparison.
         * @param {*} [meta] optional value passed directly to any callbacks invoked by this change
         * @returns {boolean} true when the operation resulted in changes to the model
         */
        save(changes, meta) {
            const delta = ø();
            const old = ø();

            Object.keys(changes).forEach(key => {
                if (!(key in attributes)) {
                    throw new Error(`unknown key: ${key}`);
                }
                const value = changes[key];
                const oldValue = attributes[key];
                if (!isEqual(value, oldValue)) {
                    delta[key] = value;
                    old[key] = oldValue;
                }
            });

            const keys = Object.keys(delta);
            if (keys.length > 0) {
                keys.forEach(key => attributes[key] = delta[key]);
                keys.forEach(key => dispatch.call(`change:${key}`, null, delta[key], old[key], meta));
                dispatch.call("change", null, delta, old, meta);
                // console.log("change", delta, old, meta)
                return true;
            }

            return false;
        }

        /**
         * @param {string} key
         * @param {*} value the new value for the key
         * @param {*} [meta] optional value passed directly to any callbacks invoked by this change
         * @returns {boolean} true when the operation resulted in changes to the model
         */
        set(key, value, meta) {
            return this.save({[key]: value}, meta);
        }

        /**
         * @param {string} key
         * @param {Function} fn ƒ(value) where the arg is the current value of the key and result is the new value
         * @param {*} [meta] optional value passed directly to any callbacks invoked by this change
         * @returns {boolean} true when the operation resulted in changes to the model
         */
        getAndSet(key, fn, meta) {
            return this.set(key, fn(this.get(key)), meta);
        }

        /**
         * @param {string} key the boolean value on this model to toggle, raising change events in the process
         * @param {*} [meta] optional value passed directly to any callbacks invoked by this change
         */
        toggle(key, meta) {
            this.getAndSet(key, v => v !== true, meta);
        }

        /**
         * @param {string} key
         * @returns {Handle}
         */
        handle(key) {
            const model = this;
            // UNDONE: create a separate dispatch object to avoid this handle colliding with handlers on the model?
            //         or just document clearly that registering listeners on the handle affects the model?
            return new class Handle {

                /**
                 * @param {string} typename
                 * @param {Function} [callback] ƒ(delta, old, meta)
                 * @returns {Handle|Function}
                 */
                on(typename, callback) {
                    typename = interpret(typename)[0];
                    const tn = `${typename.substr(0, 6)}:${key}${typename.substr(6)}`;
                    return arguments.length < 2 ? dispatch.on(tn) : (dispatch.on(tn, callback), this);
                }

                /** @returns {*} */
                get() {
                    return model.get(key);
                }

                /**
                 * @param {*} value the new value
                 * @param {*} [meta] optional value passed directly to any callbacks invoked by this change
                 * @returns {boolean} true when the operation resulted in changes to the model
                 */
                set(value, meta) {
                    return model.set(key, value, meta);
                }

                /**
                 * @param {Function} fn ƒ(value) where the arg is the current value and result is the new value
                 * @param {*} [meta] optional value passed directly to any callbacks invoked by this change
                 * @returns {boolean} true when the operation resulted in changes to the model
                 */
                getAndSet(fn, meta) {
                    return model.getAndSet(key, fn, meta);
                }

                /**
                 * @param {*} [meta] optional value passed directly to any callbacks invoked by this change
                 */
                toggle(meta) {
                    model.toggle(key, meta);
                }

            };
        }
    };
}

/*
  CONSIDER: each key is defined as an enumerable property, so the following can be done:

          model.a = 3   instead of   model.set("a", 3)
          model.a       instead of   model.get("a")

          model.save({a: 3})  still works
          model.set("a", 3)   would still be needed because is the only way to specify meta objects during set

      Only issue is name collision. Alternatively, put the enumerable properties in a prop field:

          model.prop.a = 3
          model.set("a", 3)

          model.prop.a
          model.get("a")

          model.prop
          model.getAll()

      And the model needs to be frozen to disallow setting any other values.
      Would need to make sure this works:  Object.assign({}, model);  ideally, those keys would not be properties...

      It could also be nice to wrap a handle tied directly to a field... Pretty much like a model of just one property.
      Then handles could be passed to controllers instead of model,key pairs. Set and get and "on" methods would not
      need the name of a key because it's embedded in the handle.

*/
